
昨天逛了 Firebase 商業街,知道了鏢局 Cloud Functions for Firebase 的規矩,
今天就來正式開張鏢局!
鏢局 Cloud Functions for Firebase ,該怎麼建置呢?
要準備開發環境嗎?
寫好的函式,能不能先在本機試?
部署雲端之後,App 要怎麼呼叫?會不會很慢?
今天就從零開始,看看一支函式從寫好到被 App 呼叫,會經過哪些關卡!
函式會在本機撰寫、測試,再從本機部署到雲端,這些步驟都要靠指令列工具完成。
開始之前,先把工具準備好。
要安裝的東西分成兩種:
| 類型 | 裝在哪裡 | 裝幾次 | 例子 |
|---|---|---|---|
| 工具 | 本機(全域安裝) | 一次就好,所有專案共用 | Node.js、Firebase CLI、FlutterFire CLI |
| 套件 | 專案裡 | 每個專案各自安裝 | 後端的 firebase-functions、App 的 cloud_functions |
這一節只安裝「工具」;套件會在建立專案時一起裝好。
| 工具 | 為什麼需要 | 確認指令 | 這次的版本 |
|---|---|---|---|
| Node.js | 函式用 JavaScript 撰寫,本機的模擬器、套件安裝都靠它;Firebase CLI 本身也是用 Node.js 執行 | node -v | 24.21.0 |
| Firebase CLI | 建立函式、啟動模擬器、部署到雲端 | firebase --version | 15.31.0 |
| FlutterFire CLI | 替 Flutter App 產生連到 Firebase 的設定檔 | flutterfire --version | 1.4.1 |
先確認本機的 Node.js 版本:
node -v
Firebase CLI 需要 Node.js 20 以上,
函式部署到雲端後用哪一版,則由 functions/package.json 的 engines 決定。
firebase init 產生的範本,預設就是 Node.js 24。
npm install -g firebase-tools
firebase --version
-g 代表全域安裝:裝一次,本機的所有專案都能使用。
dart pub global activate flutterfire_cli
flutterfire --version
如果終端機提示 ~/.pub-cache/bin 不在 PATH 裡,照提示加進去,才能直接執行 flutterfire。
三個確認指令都印出版本號,環境就準備好了。
| 狀況 | 原因 | 解法 |
|---|---|---|
| firebase 執行時出現 Node.js 版本不符的警告或錯誤 | 本機的 Node.js 太舊 | 升級到 20 以上(建議 24) |
| command not found: firebase 或 flutterfire | 工具的安裝位置不在 PATH 裡 | 重新開終端機;或依安裝時的提示設定 PATH |
| flutterfire 出現 Can't load Kernel binary: Invalid kernel binary format version. | Dart 升級後,舊的執行檔看起來不相容了 | 再執行一次 dart pub global activate flutterfire_cli |
寫函式之前,要先決定App 要用什麼方式呼叫這支函式。
這個決定會影響兩件事:
後端要自己處理多少事:解析送來的資料、確認呼叫的人是誰、出錯時回傳什麼。App 端怎麼呼叫:自己組出 HTTP 請求,還是透過 Firebase SDK 直接呼叫。
兩種呼叫方式(函式類型),就像銀行裡不同的窗口:
一般櫃台(onRequest):誰都能來辦,文件格式不限;但核對身分、檢查文件,都要櫃台人員自己來。會員專屬窗口(onCall):只收固定格式的申請單,會員的身分資料會自動帶上,辦不成也會用固定的格式回覆。
兩者的具體差異:
| onRequest(HTTP 函式) | onCall(Callable 函式) | |
|---|---|---|
| 誰來呼叫 | 任何 HTTP 用戶端(瀏覽器、curl、第三方服務) | App 透過 Firebase SDK 呼叫 |
| 資料格式 | 自己決定:JSON、表單、檔案都可以 | 固定用 JSON:請求放在 data,回應放在 result |
| 身分驗證 | 自己處理 | SDK 自動帶上,結果放在 request.auth |
| 出錯時 | 自己決定要回什麼 | 丟出 HttpsError,App 收到對應的錯誤代碼 |
| 適合 | webhook、公開 API、給第三方呼叫 | 自家 App 呼叫後端 |
小提醒:
自家 App 要呼叫的,選 onCall 可以省下不少功夫,今天的示範專案用的就是它。不過,會員專屬窗口只收固定格式的申請單,
也就是callable 函式只能傳 JSON 支援的型別,沒有二進位;如果要接 Cloud STT,就得先把錄音轉成 base64 字串,或先上傳到 Cloud Storage,
再把路徑交給函式。
專案 ID 用 demo- 開頭,
Firebase CLI 就會把它當成示範專案,只在本機模擬器上執行。
firebase init functions --project demo-kenkou
kenkou-functions-demo
├─ .firebaserc # 專案別名
├─ firebase.json # 專案設定
└─ functions/
├─ package.json # 相依套件與 Node.js 版本
├─ index.js # 函式程式碼
└─ .eslintrc.js # ESLint 設定
範本裡的範例函式都是註解,但留下了沒用到的 import,還有不符合 ESLint 規則的空格。直接執行 npm run lint,會出現 4 個錯誤:
'onRequest' is assigned a value but never used
'logger' is assigned a value but never used
There should be no space after '{'
There should be no space before '}'
✖ 4 problems (4 errors, 0 warnings)
因為 firebase.json 設定了部署前先跑 lint(predeploy),
範本原封不動拿去部署,會在 lint 這一步失敗。寫好自己的函式、整理掉沒用到的 import,就不會遇到這個問題。
範本的 .eslintrc.js 設定 ecmaVersion: 2018,而 ?.(optional chaining)是 ES2020 才加入的語法:
Parsing error: Unexpected token .
Node.js 24 本身支援 ?.,擋下來的只是 ESLint 的檢查,把版本調高就好:
// functions/.eslintrc.js
parserOptions: {
// ?. 是 ES2020 的語法
"ecmaVersion": 2020,
},
收到名字,回一句早安;沒給名字,就退件:
// functions/index.js
const {setGlobalOptions} = require("firebase-functions");
const {onCall, HttpsError} = require("firebase-functions/https");
const logger = require("firebase-functions/logger");
// 所有函式預設部署到台灣,最多同時 10 個執行個體
setGlobalOptions({region: "asia-east1", maxInstances: 10});
exports.greet = onCall((request) => {
const name = request.data?.name;
if (typeof name !== "string" || name.length === 0) {
// HttpsError 的 code、message 會原樣傳回 App
throw new HttpsError("invalid-argument", "缺少 name");
}
logger.info("收到 greet 請求");
return {message: `${name},早安!`};
});
| 寫法 | 說明 |
|---|---|
| region: "asia-east1" | 函式放在台灣;沒指定時預設是 us-central1。App 端呼叫時,也要指定同一個區域 |
| request.data | App 送來的資料 |
| HttpsError | App 會收到對應的錯誤代碼與訊息;其他沒處理的例外,App 只會收到 INTERNAL |
| 回傳的物件 | 放在回應的 result 裡,送回 App |
firebase emulators:start --project demo-kenkou
✔ functions[asia-east1-greet]: http function initialized
(http://127.0.0.1:5001/demo-kenkou/asia-east1/greet).
模擬器的管理介面在 http://127.0.0.1:4000,看得到 log;
修改程式碼後會自動重新載入。
callable 的協定很單純:用 POST 送出 JSON,資料放在 data 裡:
curl -H 'Content-Type: application/json' \
-d '{"data":{"name":"阿嬤"}}' \
http://127.0.0.1:5001/demo-kenkou/asia-east1/greet
| 送出 | 收到 |
|---|---|
| {"data":{"name":"阿嬤"}} | 200 {"result":{"message":"阿嬤,早安!"}} |
| {"data":{}} | 400 {"error":{"message":"缺少 name","status":"INVALID_ARGUMENT"}} |
| 改用 GET 呼叫 | 400 INVALID_ARGUMENT(callable 只收 POST) |
函式丟出的 invalid-argument,到了 HTTP 這一層,就變成 400 和 INVALID_ARGUMENT。
在後院演練沒問題,接著就要到商業街正式登記開業了。

| 欄位 | 這次的設定 | 需要留意 |
|---|---|---|
| 專案名稱 | Kenkou TW Dev | 之後可以修改 |
| 專案 ID | kenkou-tw-dev | 建立之後就不能修改;會出現在函式的網址裡 |
| Google Analytics | 關閉 | 這次的示範用不到 |
Day13 提過,部署 Cloud Functions 需要 Blaze,也介紹了防爆帳單的工具。這次設定了兩道防線:
在 Firebase 主控台的「用量與帳單」→「帳戶和預算」,
按「查看預算」,會跳到 Google Cloud 的 Budgets & caps 建立預算:


小發現:預算頁的「$」是哪一種幣別?
Google Cloud 的預算頁只寫「$」,沒有標示幣別。
Google Cloud 帳單:
Each Cloud Billing account operates in a single currency, which you can't change after you create your Cloud Billing account.
意思是:每個帳單帳戶只使用一種幣別,建立之後就不能更改。
上面第一張圖的「帳單帳戶幣別」就寫著 TWD,
所以預算頁上的「$100」,其實是新台幣 100 元。

Firebase 支出上限:
Cloud Functions for Firebase: Set caps on the underlying Cloud Run functions service.
意思是:Firebase 的函式實際上跑在 Google Cloud 的 Cloud Run functions 上,上限也是設在這裡。
| 費用 | 算在這個上限裡嗎 |
|---|---|
| 函式執行的費用 | 算 |
| 部署時用到的 Cloud Build、Artifact Registry | 不算 |
| 函式裡呼叫的其他服務(例如 Cloud STT、Gemini) | 不算 |
沒算進去的費用,要靠預算提醒盯著。
小發現:
設好之後,Google Cloud 的 Budgets & caps 清單多了一筆 Generated spend cap (Firebase Console)(下圖第二列)。
看起來 Firebase 的設定畫面只是一個捷徑,實際建立的是 Google Cloud 的預算。

firebase login
firebase use --add kenkou-tw-dev --alias default
firebase deploy --only functions
函式部署成功了,指令卻回報錯誤✔ functions[greet(asia-east1)] Successful create operation.
⚠ functions: No cleanup policy detected for repositories
in asia-east1. This may result in a small monthly bill
as container images accumulate over time.
Error: Functions successfully deployed but could not set up
cleanup policy in location asia-east1. …
函式其實已經上線了,失敗的是
清理政策:
每次部署都會在 Artifact Registry 留下一份容器映像檔,沒有定期清理,
就會慢慢累積儲存費用,也就是 Day13 提過的「小額帳單」。
firebase functions:artifacts:setpolicy \
--location asia-east1 --days 1 --force
- --days 1:自動刪除超過 1 天的映像檔(CLI 的預設值)。
- --location 預設是 us-central1,
函式不在預設區域時,一定要指定,不然會設到錯的地方。設好之後,再部署就不會出現這個錯誤了。
curl -H 'Content-Type: application/json' \
-d '{"data":{"name":"阿嬤"}}' \
https://asia-east1-kenkou-tw-dev.cloudfunctions.net/greet
| 呼叫 | 結果 |
|---|---|
| 部署後第 1 次 | 200,0.41 秒 |
| 第 2~6 次 | 200,約 0.08 秒 |
| 不帶名字 | 400,INVALID_ARGUMENT/缺少 name |
注意:這時候,任何人只要知道網址,用 curl、Postman 就能呼叫這支函式。網址格式是固定的,專案 ID 也打包在 App 裡,藏不起來;如果函式呼叫的是 Gemini 這類付費服務,別人一直打,帳單可能就暴增。
flutter pub add firebase_core cloud_functions
flutterfire configure --project=kenkou-tw-dev \
--platforms=ios,android \
--ios-bundle-id=tw.aarontsai.kenkouFunctionsDemo \
--android-package-name=tw.aarontsai.kenkou_functions_demo
flutterfire configure 會替各平台在 Firebase 註冊 App,並產生設定檔:
| 檔案 | 說明 |
|---|---|
| lib/firebase_options.dart | 各平台的 Firebase 設定 |
| ios/Runner/GoogleService-Info.plist | iOS 的設定檔 |
| android/app/google-services.json | Android 的設定檔 |
| android/settings.gradle.kts 等 | 加上 Google Services 的 Gradle 外掛 |
App 啟動時初始化 Firebase:
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
// 函式在 asia-east1,App 端也要指定同一個區域
final functions =
FirebaseFunctions.instanceFor(region: 'asia-east1');
try {
final result = await functions
.httpsCallable('greet')
.call({'name': '阿嬤'});
print(result.data['message']);
} on FirebaseFunctionsException catch (e) {
// e.code 對應後端 HttpsError 的代碼
print('${e.code}: ${e.message}');
}
App 端指定的區域要跟函式一致:
用 FirebaseFunctions.instance 的話,預設會去 us-central1 找函式。
執行後,App 收到「阿嬤,早安!」,就代表 App 成功呼叫到雲端的函式了。
今天鏢局正式開張,從零走到 App 呼叫成功:
工具備齊、本機演練過關、雲端正式開張:App 託付的第一趟鏢,順利送達,也平安帶了回來!
開張第一天,也發現了一件讓人不太放心的事:任何人只要知道網址,都能直接呼叫這支函式,不一定是自家的 App。
所以,鏢局需要增添守衛,確認上門的真的是自家的 App;鏢局裡值錢的金鑰,也需要一個保險箱。
明天就來替鏢局請守衛、打造保險箱:Cloud Functions for Firebase(下)!
感謝有緣看到這邊的你~
希望佛菩薩也祝福你:🌟平安開心 幸福順遂🌟
南無觀世音菩薩🍀 南無地藏菩薩🏠 南無阿彌陀佛☀️